from general_utilities import *
from file_info import *
from map import *
from noises import *
from screen_info import display
from sound import sound
from notifications import *
from ai import *
from powers import *
from monster_list import *
from conditions import *
import random
from math import ceil, exp

class Entity_id:
  def __init__(self,group,member):
    self.group = group
    self.member = member

class Entity:
  def __init__(self,d,group_id,member_id,pos,type,attitude,enemy_groups):
    # Any new fields here for character - consider adding to new_dungeon() in gamestate.py
    self.type = type # a "M_" string
    self.is_character = 0 # override for characters
    self.name = self.info().name
    self.pos = pos.copy()
    self.id = Entity_id(group_id,member_id)
    self.attitude = attitude  # A_FRIENDLY, A_NEUTRAL or A_HOSTILE
    self.enemy_groups = enemy_groups  # vector of group ID
    self.energy = float(0)
    self.turn_at_last_after_action_update = long(0)
    self.charmed_by = None
    self.c = {}  # keyed by Condition types
    self.action_queue = [] # may be filled in with delayed actions later
    self.is_no_longer_here = 0
    if type==M_CHARACTER:
      self.brain = None
    else:
      self.brain = make_brain(d,self)
    self.vulnerable=0 # can be set to 1 by the AI, meaning they take extra damage if meleed
    if type==M_CHARACTER:
      self.original_hp = 0 # will set these in Character.__init__
      self.original_mp = 0
      self.hp = 0
      self.mp = 0
      self.heal_max_hp = 0
    else:
      self.original_hp = random_positive_int_around(1.5*self.info().avg_original_hp,0.2)
      self.original_mp = random_positive_int_around(self.info().avg_original_mp,0.2)
      self.hp = self.original_hp
      self.mp = self.original_mp
      self.heal_max_hp = self.original_hp
    d.entity_positions[self.pos.to_tuple()] = self
    d.update_entity_rooms(self,None,self.pos)

  def info(self):
    # overload this for Characters
    return mlib.m[self.type]
    
  def __str__(self):
    return "(Entity " + self.name + " with group_id " + str(self.id.group) + ", member_id " + str(self.id.member) + ")"
      
  def __eq__(self,other):
    if other is None:
      return 0
    return self.id.group==other.id.group and self.id.member==other.id.member

  def __ne__(self,other):
    if other is None:
      return 1
    return self.id.group<>other.id.group or self.id.member<>other.id.member

  def get_displaynum(self):
    # only defined for characters
    return None
  
  def group_of(self,d):
    return d.gr[self.id.group]
  
  def begin_new_turn(self,d):
    # return 1 if they get to do something
    if not (self.no_longer_here()):
      self.check_conditions_expiry(d) 
      if self.has_condition(C_POISONED):
        if (self.c[C_POISONED].level==1 and random.random()<0.125) or (self.c[C_POISONED].level==2 and random.random()<0.175):
          if self.is_character:
            n = Notification(self.pos)
            n.set_sound("you_poison")
            n.play(d)
          self.take_damage(d,1,armor_helps=0)
          if self.no_longer_here():
            return 0
      if not (self.no_energy_gain()):
        # handle energy gain
        self.energy = self.energy + 1.0
        if self.mp < self.original_mp:
          self.mp = min(self.mp+0.01*self.original_mp*(1+self.info().mp_regeneration_boost),self.original_mp)
        if self.energy > 0: #  and self.is_character: # TO DO - remove last condition after debugging
          return 1
    return 0

  def after_action_update(self,d):
    # this gets run after they've done an action (they may still get more actions in the turn if they have enough energy)
    turns_elapsed = d.turn - self.turn_at_last_after_action_update
    if turns_elapsed:
      if not self.no_longer_here():
        if d.l.tile_at(self.pos).info().is_fiery or d.l.is_temporary_fire(self.pos):
          if self.is_character:
            n = Notification(self.pos)
            if self.info().fire_immunity:
              n.set_message(self.name+" is untouched by the fire!",0)
            elif self.info().fire_resistance:
              n.set_message(self.name+" is a little bit singed by the fire.",0)
            else:
              n.set_message(self.name+" is burnt by the fire!",0)
            n.set_sound('118-Fire02')
            n.play(d)
          self.take_damage(d,random_positive_int_around(3*turns_elapsed,0.4),fire_resistance_helps=1)
      self.turn_at_last_after_action_update = d.turn

  def spend_energy(self,amount):
    self.energy = self.energy - amount
    
  def get_energy(self,amount):
    self.energy = self.energy + amount    
  
  def is_enemy(self,e2):
    # does this entity regard e2 as an enemy?
    if self.out_of_control():    
      if self.charmed_by == e2.id.group:
        return 0
    if self.attitude <> e2.attitude:
      return 1
    if self.attitude==A_NEUTRAL and self.id.group <> e2.id.group:
      return 1
    if e2.id.group in self.enemy_groups:
      return 1
    return 0
    
  def can_move(self,d,pos):
    # return a move code, and the entity stepped into if any
    # assumes the tile is adjacent
    if pos is None or not d.l.valid(pos):
      return M_FAIL, None
    tile = d.l.tile_at(pos)
    occupant = d.get_occupant(pos)
    if occupant is not None and self.is_enemy(occupant) and (not tile.info().is_portal or not d.l.tile_at(self.pos).info().is_portal):
      return M_HIT, occupant
    if not tile.info().walkable(self):
      return M_FAIL, None
    if occupant is None:
      return M_EASY, None
    if self.can_push(d,occupant) or (not occupant.is_character and tile.info().is_portal): 
      return M_PUSH, occupant
    return M_FAIL, None 
  
  def can_push(self,d,e2):
    # can this entity force e2 to swamp places, assuming that they are friendly?
    if self.is_character:
      return 1
    if e2.is_character:
      return 0
    if e2.info().immobile:
      return 0
    if self.id.group < e2.id.group:
      return 1
    if self.id.group > e2.id.group:
      return 0
    if self.id.member < e2.id.member:
      return 1
    if self.id.member > e2.id.member:
      return 0

  def get_melee_hit_chance(self,d,target):
    result = self.info().melee_hit_chance
    if self.has_condition(C_BLESSED):
      result = min(1-0.5*(1.0-result),result + 0.2)
    if target.has_condition(C_BLESSED):
      result = max(result/2.0,result - 0.2)
    if target.has_condition(C_DEFENDING) or target.info().evasive:
      result = result / 2.0
    return result
  
  def get_missile_hit_chance(self,d,target):
    result = self.info().missile_hit_chance
    if d.tutorial:
      result = result + 0.2
    if self.has_condition(C_BLESSED):
      result = min(1-0.5*(1-result),result + 0.2)
    if target.has_condition(C_BLESSED):
      result = max(result/2.0,result - 0.2)
    if target.has_condition(C_DEFENDING) or target.info().evasive:
      result = result / 2.0
    if target.info().protected_from_missiles:
      result = result / 2.0
    return result

  def add_condition(self,d,type,fixed_duration,expected_random_duration,charmer_group=None,poison_level=1):
    if self.brain is not None:
      self.brain.disturbed = 1
    end_turn = -1
    if type==C_DEAD:
      self.c[type] = Condition_dead(d,self.id,type,end_turn)
    elif type==C_ESCAPED:
      self.c[type] = Condition_escaped(d,self.id,type,end_turn)
    elif type==C_UNCONSCIOUS:
      self.c[type] = Condition_unconscious(d,self.id,type,end_turn)
    else:
      end_turn = d.turn + fixed_duration
      if expected_random_duration>0:
        end_turn = end_turn + random.expovariate(1.0/expected_random_duration)
      if type==C_ASLEEP:
        self.remove_incapacitations(d)
        self.c[type] = Condition_asleep(d,self.id,type,end_turn)
      elif type==C_PARALYSED:
        self.remove_incapacitations(d)
        self.c[type] = Condition_paralysed(d,self.id,type,end_turn)
      elif type==C_CONFUSED:
        self.remove_incapacitations(d)
        self.c[type] = Condition_confused(d,self.id,type,end_turn)
      elif type==C_AFRAID:
        self.remove_incapacitations(d)
        self.c[type] = Condition_afraid(d,self.id,type,end_turn)
      elif type==C_CHARMED:
        self.remove_incapacitations(d)
        self.c[type] = Condition_charmed(d,self.id,type,end_turn,charmer_group) # different constructor
      elif type==C_PARALYSED:
        self.remove_incapacitations(d)
        self.c[type] = Condition_paralysed(d,self.id,type,end_turn)
      elif type in [C_DEFENDING,C_CIRCLE_OF_PROTECTION,C_DODGING,C_SNEAKING,C_FIRERES_FROM_SPELL,C_FIRERES_FROM_POTION,C_CHAMELEON,C_STONESKIN,C_BLESSED,C_INVISIBLE]:
        self.c[type] = Condition_defense(d,self.id,type,end_turn)
        if self.is_character:
          self.stats.recalc(d)
        if type==C_INVISIBLE:
          display.redraw_tile(d,self.pos,self)
          display.show_glyphs(d)
          display.blit_map_to_screen() 
      elif type==C_POISONED:
        if not type in self.c.keys():
          self.c[type] = Condition_poisoned(d,self.id,type,end_turn,poison_level)
        else:
          self.c[type].level = max(poison_level,self.c[type].level)
          old_end_turn = self.c[type].end_turn
          new_end_turn = old_end_turn + (end_turn-d.turn)/2
          self.c[type].end_turn = new_end_turn                     
      else:
        self.c[type] = Condition(d,self.id,type,end_turn)
    self.update_pane(d) 
  
  def check_conditions_expiry(self,d):
    # run this for every Entity every turn
    for type in self.c.keys():
      end_turn = self.c[type].end_turn
      if end_turn <= d.turn and end_turn>=0:
        self.c[type].expire(d,display,self)
        del self.c[type]
        self.update_pane(d)
    if self.is_character:
      self.stats.recalc(d)
        
  def remove_condition_if_present(self,d,type,quiet=0):
    if type in self.c:
      self.c[type].end(d,display,self,quiet)
      del self.c[type]
    self.update_pane(d)  

  def remove_incapacitations(self,d):
    # quietly remove anything incapacitating - usually used when something else incapacitating is about to start!
    for type in self.c.keys():
      if self.c[type].no_energy_gain() and not self.c[type].no_longer_here():
        self.c[type].end(d,display,self,1)

  def get_conditions_string(self,d):
    result = ""
    for type in self.c.keys():
      result = result + self.c[type].get_type_string() + "  "
    return result
  
  def is_dead(self):
    for type in self.c.keys():
      if self.c[type].is_dead():
        return 1
    return 0
  
  def has_escaped(self):
    for type in self.c.keys():
      if self.c[type].has_escaped():
        return 1
    return 0
  
  def no_longer_here(self):
    if self.is_no_longer_here:
      return 1
    if len(self.c)==0:
      return 0
    for type in self.c.keys():
      if self.c[type].no_longer_here():
        self.is_no_longer_here = 1
        return 1
    return 0

  def has_condition(self,condition):
    for type in self.c.keys():
      if type==condition:
        return 1
    return 0

  def no_energy_gain(self):
    for type in self.c.keys():
      if self.c[type].no_energy_gain():
        return 1
    return 0
    
  def appears_incapacitated(self):
    for type in self.c.keys():
      if self.c[type].appears_incapacitated():
        return 1
    return 0
    
  def out_of_control(self):
    for type in self.c.keys():
      if self.c[type].out_of_control():
        return 1
    return 0
  
  def remove(self,d):
    # died, escaped, etc
    self.energy = 0
    del d.entity_positions[self.pos.to_tuple()]
    d.update_entity_rooms(self,self.pos,None)
    display.redraw_tile(d,self.pos,None) 
    if self.is_character:
      display.refill_character_pane(d)
      display.draw_map(d)    
    else:
      display.redraw_tile(d,self.pos,None) 
    display.show_glyphs(d)
    display.blit_map_to_screen() 
    if self.is_character and self.is_dead():
      files.end_game(d,display,1)
      
  def die(self,d):
    if not self.is_character:
      if self.info().key_carrier: # it must drop, even if the current space is not suitable
        drop_pos = self.pos.copy()
        while 1:
          if d.l.valid(drop_pos) and d.l.tile_at(drop_pos).info().can_contain_objects:
            d.l.tile_at(drop_pos).object = Object(O_KEY_GOLD)
            break
          drop_pos.right = random.randint(0,d.l.room_at(drop_pos).width-1)
          drop_pos.down = random.randint(0,d.l.room_at(drop_pos).height-1)
      elif self.info().treasure_carrier and d.l.tile_at(self.pos).info().can_contain_objects and d.l.tile_at(self.pos).object is None:
        object_types = [O_GOLD,O_SILVER,O_COPPER,O_CHEST_SMALL,O_HEALING_POT]
        object_probs = [0.3,   0.5,     0.3,     0.2,          0.1]
        while 1:
          i = random.randint(0,len(object_types)-1)
          if random.random()<object_probs[i]:
            object_type = object_types[i]
            break
        d.l.tile_at(self.pos).object = Object(object_type)
      if self.brain.fade_out():
        display.fade(d,self)
    if self.is_character:
      n = Notification(self.pos)
      n.set_message(self.name+" dies...",0) 
      n.play(d)
      d.l.tile_at(self.pos).object = Object(O_YOU_SKELETON)
      display.fade(d,self)
      n = Notification(self.pos) 
      n.set_sound("gong")
      n.play(d)
      wait(ms=1300)
      n = Notification(self.pos) 
      n.set_sound("gong")
      n.play(d)
      wait(ms=1300)
      n = Notification(self.pos)  
      n.set_sound("gong")
      n.play(d)
      wait(ms=3000)
    self.add_condition(d,C_DEAD,None,None,None)   
    self.remove(d)
    if not self.is_character:
      self.brain.death_message(d) 
    
  def take_damage(self,d,damage,armor_helps=0,magic_resistance_helps=0,fire_resistance_helps=0):
    # fire_resistance_helps can be 1 (big help) or 0.5 (small help)
    # return 1 if the damage is minimal
    if fire_resistance_helps==1:
      if self.info().fire_resistance:
        damage = damage / 3.0
      elif self.info().fire_immunity:
        return 1
    elif fire_resistance_helps==0.5:
      if self.info().fire_resistance:
        damage = damage * 0.7
      elif self.info().fire_immunity:
        damage = damage * 0.4
    if armor_helps:
      damage = damage - (self.info().armor - 3)/ 2.0
    if magic_resistance_helps:
      damage = damage - (self.info().magic_resistance-3)/ 2.0
    if damage > -1:
      damage = max(1,ceil(damage))
    elif damage > -2:
      damage = max(0.5,ceil(damage))
    elif damage > -3:
      damage = max(0.25,ceil(damage))
    else:
      damage = 0
    self.hp = self.hp - damage
    if self.brain is not None:
      self.brain.disturbed = 1
    self.heal_max_hp = self.heal_max_hp - damage/2
    if self.has_condition(C_SNEAKING):
      self.remove_condition_if_present(d,C_SNEAKING)
      self.stats.recalc(d)
      display.redraw_tile(d,self.pos,self)
      display.show_glyphs(d)
      display.blit_map_to_screen()
      wait(ms=500)
    if self.is_character:
      self.update_pane(d) 
      d.clear_action_stack()
      for entity in d.get_entities_in_room(self.pos): # wakes everyone up
        if not entity.no_longer_here():
          gr = d.get_group(entity.id.group)
          gr.enemy_seen(d,self)
    if self.hp <= 0:
      self.die(d)
      return 0
    elif damage < 1:
      return 1
    else:
      return 0
  
  def make_magic_resistance_check(self,difficulty):
    # difficulty of 5=easy,10=medium,15=hard,20=extremely hard
    difficulty_for = 0.4*(difficulty - self.info().magic_resistance)
    return random.random() > (exp(difficulty_for)/(1+exp(difficulty_for)))
  
  def get_poisoned(self,d,poison_level):
    # so far poison_level can be 1 or 2
    n = Notification(self.pos)
    n.set_message(self.name + " is poisoned!",0)
    if self.is_character:
      n.set_sound("you_poison")
    else:
      n.set_sound("monst_poison")
    n.play(d)
    if self.has_condition(C_POISONED) and random.random()<0.1:
      poison_level = 2
    self.add_condition(d,C_POISONED,30*poison_level,30*poison_level,poison_level=poison_level)
    
  def escape(self,d):
    self.add_condition(d,C_ESCAPED,None,None,None)
    self.remove(d)    

  def make_run_away(self,d):
    # overload for characters
    self.add_condition(d,C_AFRAID,40,30)
    run_away = Priority_run_away(self.brain,urgency=PU_OVERRIDING,while_condition_lasts=C_AFRAID,locked=1)
    self.brain.priorities.append(run_away)

  def update_pane(self,d): 
    # overload for characters
    pass
  
  def use_zap(self,need,n):
    # overload for characters
    return 0

  def stealth_sound_multiplier(self):
    # overload for characters
    return 1
  
  #def enemies_seen(self,d):
    ## return a [Entity] or []
    #result = []
    #for gr in d.gr:
      #for entity in gr.m:
        #if entity.pos.same_room(self.pos):
          #if entity is not None and not entity.no_longer_here() and entity is not self:
            #if self.can_see_if_in_los(d,entity):
              #if self.is_enemy(entity):
                #result.append(entity)
    #return result
  
  def can_see_if_in_los(self,d,entity):
    # can this entity be seen, assuming it is in LOS?
    # check invisibility, darkness, etc
    return 1 # for now

  def move(self,d,new_pos,bumped_entity):
    # entity moves to new_pos, displacing bumped_entity
    # check whether anyone saw it and update threats if so
    # no need to deduct energy here
    # no check that the move is possible - should be done at a different level
    if new_pos==self.pos:  # Shouldn't happen, but best catch it here if it does
      return
    old_pos = self.pos.copy()
    display.redraw_tile(d,old_pos,None)
    self.pos = new_pos.copy()
    del d.entity_positions[old_pos.to_tuple()] # keep entity_positions updated!
    d.entity_positions[new_pos.to_tuple()] = self
    d.update_entity_rooms(self,old_pos,new_pos)
    # handle perceptions - did anyone see this move and register it as a threat?
    for seer in d.get_entities_in_room(new_pos): 
      if not seer.no_longer_here() and not seer.brain is None and not seer.brain.eyes_shut:
        if seer.can_see_if_in_los(d,self):
          if seer.is_enemy(self):
            gr = d.get_group(seer.id.group)
            gr.enemy_seen(d,self)
            break
    n = Notification(new_pos)
    if self.is_character and d.l.tile_at(new_pos).info().refreshing:
      self.heal_max_hp = min(self.heal_max_hp+0.2*self.original_hp,self.original_hp)
      self.hp = min(self.hp+random.randint(6,16),self.heal_max_hp)
      self.mp = min(self.mp+0.2*self.original_mp,self.original_mp)
      self.remove_condition_if_present(d,C_POISONED)
      n = Notification(self.pos)
      n.set_message(self.name+" is invigorated by the clear and sparkling water!",0)
      n.set_sound("fanfare2")
      n.play(d)      
      self.stats.recalc(d)
    elif self.is_character and d.l.tile_at(new_pos).info().can_be_unlocked:
      # if they are here they must be able to unlock it
      locked_door = d.l.tile_at(new_pos)
      if locked_door.info().is_portal:
        d.l.room_at(new_pos).tiles[new_pos.down][new_pos.right] = Tile(T_DOOR,locked_door.wall_type,locked_door.floor_type)
        d.l.room_at(new_pos).tiles[new_pos.down][new_pos.right].to_pos = locked_door.to_pos
        other_side = d.l.tile_at(locked_door.to_pos)
        d.l.room_at(locked_door.to_pos).tiles[locked_door.to_pos.down][locked_door.to_pos.right] = Tile(T_DOOR,other_side.wall_type,other_side.floor_type)
        d.l.room_at(locked_door.to_pos).tiles[locked_door.to_pos.down][locked_door.to_pos.right].to_pos = other_side.to_pos
        d.room_network.nodes[(new_pos.level,new_pos.r_down,new_pos.r_right)] = None 
        d.room_network.nodes[(locked_door.to_pos.level,locked_door.to_pos.r_down,locked_door.to_pos.r_right)] = None 
      else:
        # it's a gate
        d.l.room_at(new_pos).tiles[new_pos.down][new_pos.right] = Tile(T_FLOOR,locked_door.wall_type,locked_door.floor_type)
        d.room_network.nodes[(new_pos.level,new_pos.r_down,new_pos.r_right)] = None 
      if locked_door.info().key_type is not None:
        for i in range(len(self.keys)):
          key = self.keys[i]
          if locked_door.info().is_key_to(key):
            self.keys[i] = None
            break
        n = Notification(self.pos)
        n.set_message(self.name+" unlocks the door...",0)
        n.set_sound("door_unlocking")
        n.play(d)      
        self.stats.recalc(d)
      else:
        n = Notification(self.pos)
        n.set_message(self.name+" opens the door...",0)
        n.set_sound("door_unlocking")
        n.play(d)    
    elif d.l.tile_at(new_pos).info().fall_into and (self.is_character or not self.info().flying):
      n.set_noise(1,SD_CRASH,15,self,0,1)
      n.set_message(self.name+" falls into a pit!",0)
      n.set_sound('earth07',volume_mult=1.3)
      n.play(d)      
      if self.is_character:
        damage = 5
	n.set_sound('fall',volume_mult=1.3)
      elif self.info().size==SZ_SMALL:
        damage = 3
      elif self.info().size==SZ_MEDIUM:
        damage = 5
      elif self.info().size==SZ_LARGE:
        damage = 8
      self.take_damage(d,random_positive_int_around(damage,0.5),armor_helps=0)
      self.energy = self.energy - 2
    elif self.is_character:
      n.set_noise(0.3,SD_MOVE,5,self,0,1)
      n.play(d)
    else:
      if random.random()<0.1:
        if self.brain.move_sound is not None:
          n.set_noise(1,self.brain.move_sound,self.brain.move_volume,self,1,1)  
        if self.brain.move_message is not None:
          n.set_message(self.brain.move_message,0)
    if bumped_entity is not None:
      bumped_new_pos = None
      if self.is_enemy(bumped_entity) or not old_pos.same_room(new_pos):
        bumped_new_pos = scatter_adjacent(d,bumped_entity,new_pos) # may return None
      if bumped_new_pos is None:
	if self.is_character:
	  display.send_message("POSSIBLE BUG ALERT! You swapped places with a monster") 
        bumped_new_pos = old_pos
      bumped_entity.pos = bumped_new_pos.copy()
      d.entity_positions[bumped_new_pos.to_tuple()] = bumped_entity # keep entity_positions updated
      d.update_entity_rooms(bumped_entity,new_pos,bumped_new_pos)
      # again, handle perceptions - did anyone see this move and register it as a threat?
      for seer in d.get_entities_in_room(bumped_new_pos): 
        if not seer.no_longer_here() and not seer.brain is None and not seer.brain.eyes_shut:
          if seer.can_see_if_in_los(d,bumped_entity):
            if seer.is_enemy(bumped_entity):
              gr = d.get_group(seer.id.group)
              gr.enemy_seen(d,bumped_entity)
              break
      n = Notification(bumped_new_pos)
      n.set_noise(0.1,SD_MOVE,10,bumped_entity,not bumped_entity.is_character,1)
      n.play(d)
      display.redraw_tile(d,bumped_new_pos,bumped_entity) 
      if bumped_entity.is_character:
        d.l.room_at(bumped_new_pos).ever_visited = 1
        display.move_view_to(d,bumped_new_pos)
    display.redraw_tile(d,new_pos,self)
    display.show_glyphs(d)
    display.blit_map_to_screen() 
    if self.is_character:
      object_here = d.l.tile_at(new_pos).object
      if object_here is not None:
        self.pickup(d,new_pos,object_here)
      d.l.room_at(new_pos).ever_visited = 1
      if not new_pos.level in d.levels_visited:
        d.levels_visited.append(new_pos.level)
      display.move_view_to(d,new_pos)
      if d.l.tile_at(new_pos).message is not None:
        if d.l.tile_at(new_pos).type==T_SIGNPOST:
          display.simple_info_dialog('There is a signpost here!','The sign says: "' + d.l.tile_at(new_pos).message + '"')      
        elif d.l.tile_at(new_pos).type==T_GRAVEMARKER:
          display.simple_info_dialog('There is something engraved on the gravestone!','The inscription says: "' + d.l.tile_at(new_pos).message + '"')      
    if d.l.tile_at(new_pos).info().is_teleport_rune and (self.is_character or not self.info().flying):
      already_revealed = d.l.tile_at(new_pos).revealed
      if new_pos.same_room(d.get_character().pos):
	d.l.tile_at(new_pos).revealed = 1
      n = Notification(self.pos)
      n.set_noise(1,SD_CAST,10,self,1,1)
      n.set_message(self.name+" stood on a Rune of Apportation!",0)
      n.set_sound("teleport1")
      n.play(d)
      if random.random()<0.15 and already_revealed:
        n = Notification(self.pos)
        n.set_message(self.name+" resists the spell!",0)
        n.set_sound("cancel1")
        n.play(d)
      else:
        target_pos = new_pos.copy()
        count = 0
        while count<=100:
          count = count + 1
          if d.l.valid(target_pos):
            if target_pos<>self.pos:
              if d.l.tile_at(target_pos).info().walkable(self):
                if not d.l.tile_at(target_pos).info().no_teleport:
                  occupant = d.get_occupant(target_pos)
                  if occupant is None:
                    self.move(d,target_pos,None)
                    wait(500) 
                    break
          target_pos.right = random.randint(0,d.l.room_at(target_pos).width-1)
          target_pos.down = random.randint(0,d.l.room_at(target_pos).height-1)  
    elif d.l.tile_at(new_pos).info().is_confusion_rune and (self.is_character or not self.info().flying):
      if new_pos.same_room(d.get_character().pos):
	d.l.tile_at(new_pos).revealed = 1
      n = Notification(self.pos)
      n.set_message(self.name + " stood on a Rune of Mesmerism!",0)
      n.set_flash(self.pos,display.confuse,0.2)
      n.set_sound("confuse")  
      n.play(d)  
      if self.make_magic_resistance_check(13) or self.info().immune_to_enchantments:
        n = Notification(self.pos)
        n.set_message(self.name+" resists the spell!",0)
        n.set_sound("cancel1")
        n.play(d)
      else:  
        self.add_condition(d,C_CONFUSED,4,10)
      wait(1500)
    elif d.l.tile_at(new_pos).info().is_pain_rune and (self.is_character or not self.info().flying):
      if new_pos.same_room(d.get_character().pos):
	d.l.tile_at(new_pos).revealed = 1
      n = Notification(self.pos)
      n.set_message("Aaaargh! " + sentencecase(self.name) + " stood on a Symbol of Painful Warning!",0)
      n.set_flash(self.pos,display.warning,0.2)
      n.set_sound("084-Monster06")  
      n.play(d)  
      if self.is_character and self.make_magic_resistance_check(15):
        n = Notification(self.pos)
        n.set_message(self.name+" resists the spell!",0)
        n.set_sound("cancel1")
        n.play(d)
      else:  
        damage = random_positive_int_around(5,0.5)
        self.take_damage(d,damage)
        self.energy = self.energy - 2
      wait(1500)
       
  def consume(self,d,object):
    n = Notification(self.pos)
    if object.type==O_BLINK_POT:
	n.set_sound("pop")
	do_wait = 0
    else:
    	n.set_sound("bottle")
	do_wait = 1
    n.set_message(self.name+" uses the "+object.info().name+"...",0)
    n.play(d)
    if do_wait:
      wait(1500)
    if object.type==O_BLINK_POT:
      self.action_queue = [Action(self,A_BLINK,None,self)] + self.action_queue
    elif object.type==O_HEALING_POT:
      self.hp = min(self.hp+30,self.original_hp)
      self.heal_max_hp = min(self.heal_max_hp+30,self.original_hp)
      n = Notification(self.pos)
      n.set_message(self.name+" is healed!",0)
      n.set_sound("fanfare2")
      n.play(d)
    elif object.type==O_ANTIDOTE_POT:
      self.remove_condition_if_present(d,C_POISONED)
      n = Notification(self.pos)
      n.set_message(self.name+" is cured!",0)
      n.set_sound("fanfare2")
      n.play(d)
    elif object.type==O_MAGIC_POT:
      self.mp = self.original_mp
      n = Notification(self.pos)
      n.set_message(self.name+" is invigorated!",0)
      n.set_sound("fanfare2")
      n.play(d)
    elif object.type==O_CHAMELEON_POT:
      self.add_condition(d,C_CHAMELEON,100,100)
      display.redraw_tile(d,self.pos,self)
      display.show_glyphs(d)
      display.blit_map_to_screen()
    elif object.type==O_FIRERES_POT:
      self.add_condition(d,C_FIRERES_FROM_POTION,100,100)
    elif object.type==O_STONESKIN_POT:
      self.add_condition(d,C_STONESKIN,50,50)

  def popup_image(self):
    return self.info().tile
    
  def popup_desc(self):
    return self.info().description
  

class Group:
  def __init__(self,d,group_id,attitude,enemy_groups):
    self.group_id = group_id # this indexes the dungeon's vector of groups, of which element 0 is the party (character's group)
    self.attitude = attitude  # A_FRIENDLY, A_NEUTRAL or A_HOSTILE - individuals may change from this later
    self.enemy_groups = enemy_groups  # vector of group IDs - individuals may change from this later
    self.m = []  # vector of Entity
    self.extinct = 0 # just a shortcut, not guaranteed up to date, could get the most current version by checking each individual member's state
                     # Never to be set back to 0 after being set to 1
    self.moved_this_turn = []
    self.enemies_seen_recently = {} # Key is entity ID, value is (Pos,turn). New entries added by enemy_seen().
    
  def n_members(self):
    return len(self.m)

  def add_member(self,d,pos,type):
    self.m.append(Entity(d,self.group_id,self.n_members(),pos,type,self.attitude,self.enemy_groups))
  
  def get_next_actor(self,d):
    # This function returns the next group member to act, or None if there is noone left to act this turn.
    if self.extinct:
      return None
    next_char = None
    is_extinct = 1
    for member in self.m:
      if not member.no_longer_here():
        is_extinct = 0
        if not member.id in self.moved_this_turn:
          next_char = member
    if next_char is not None:
      self.moved_this_turn.append(next_char.id)
    if is_extinct == 1:
      self.extinct = 1
    return next_char
  
  def start_of_turn_update(self,d):
    # Call at the start of each turn before any entities in the group act
    # So far: delete old entries from self.enemies_seen_recently
    entity_ids_seen_recently = self.enemies_seen_recently.keys()
    for entity_id in entity_ids_seen_recently:
      (pos, turn) = self.enemies_seen_recently[entity_id]
      # print "Group ",self.group_id," has seen ",d.get_entity(entity_id).name," at ",pos," in turn ",turn
      if turn < d.turn-50:
        del self.enemies_seen_recently[entity_id]
    
  def enemy_seen(self,d,entity):
    # called by entities in the group when they see some stuff
    self.enemies_seen_recently[entity.id] = (entity.pos,d.turn)
    for monster in self.m:
      if monster.brain is not None:
        monster.brain.disturbed = 1
    
class Stats:
  def __init__(self,d,entity,name):
    self.entity = entity
    self.description = "That is you!"
    self.size = SZ_MEDIUM
    self.name = name
    self.powers = [P_SHOT, P_HARD_JUMP] # fill it in with P_ codes later
    self.first_call = 1 # don't display it first time round
    self.hearing = 2.0 # good
    self.evasive = 0
    self.melee_cost = 1
    self.missile_cost = 1
    self.immobile = 0
    self.moves_through_flame_only = 0
    self.can_swim = 0 
    self.moves_through_water_only = 0
    self.web_walk = 0
    self.immune_to_poison = 0
  
  def recalc(self,d,quiet=0):
    self.new_powers = [P_SHOT, P_HARD_JUMP] # fill in with P_ codes
    for power in self.entity.learned_powers:
      self.new_powers.append(power)
    items_worn = []
    self.speed_texts = []
    self.armor_texts = []
    self.magic_resistance_texts = []
    self.stealth_texts = []
    if self.entity.garb is None:
      self.speed = 13
      self.armor = 5
      self.magic_resistance = 5
      self.stealth = 11
      self.role = R_PEASANT
    else:
      items_worn.append(self.entity.garb)
      self.speed = self.entity.garb.info().speed
      self.armor = self.entity.garb.info().armor
      self.magic_resistance = self.entity.garb.info().magic_resistance
      self.stealth = self.entity.garb.info().stealth
      self.role = self.entity.garb.info().role
      self.add_powers(self.entity.garb)
    fireres = 0
    if self.entity.has_condition(C_FIRERES_FROM_SPELL):
      fireres = fireres+1
    if self.entity.has_condition(C_FIRERES_FROM_POTION):
      fireres = fireres+1
    if self.entity.has_condition(C_CIRCLE_OF_PROTECTION):
      self.armor = self.armor + 5
      self.armor_texts.append("Circle of Protection")
      self.magic_resistance = self.magic_resistance + 5
      self.magic_resistance_texts.append("Circle of Protection")
    if self.entity.has_condition(C_STONESKIN):
      self.armor = self.armor + 10
      self.armor_texts.append("Stoneskin")
      fireres = fireres+1
      self.speed = self.speed - 3
      self.speed_texts.append("Slowed by Stoneskin")
    if self.entity.has_condition(C_SNEAKING) or self.entity.has_condition(C_CHAMELEON):
      self.stealth = self.stealth + 5
      if self.entity.has_condition(C_SNEAKING):
        self.stealth_texts.append("Sneaking")
      elif self.entity.has_condition(C_CHAMELEON):
        self.stealth_texts.append("Chameleon")
      if self.entity.has_condition(C_CHAMELEON):
        self.stealth = max(self.stealth,15)
    self.move_cost = 10.0 / self.speed
    display.character_tile = display.role_images[self.role]
    if self.entity.melee_weapon is None:
      self.avg_melee_damage = 2
      self.melee_hit_chance = 0.5
    else:
      items_worn.append(self.entity.melee_weapon)
      self.avg_melee_damage = self.entity.melee_weapon.info().damage
      self.melee_hit_chance = 1
      self.add_powers(self.entity.melee_weapon)
    if self.entity.missile_weapon is None:
      self.avg_missile_damage = 2
      self.missile_hit_chance = 0.2
    else:
      items_worn.append(self.entity.missile_weapon)
      self.avg_missile_damage = self.entity.missile_weapon.info().damage
      self.missile_hit_chance = 0.5 + self.entity.missile_weapon.info().hit_bonus
      self.add_powers(self.entity.missile_weapon)
    for item in self.entity.items:
      if item is not None:
        items_worn.append(item)
        self.add_powers(item)
    self.mp_regeneration_boost = 0
    self.protected_from_missiles = 0
    self.immune_to_enchantments = 0
    self.melee_damage_boost = 0
    self.xp_boost = 0
    self.poisoned_melee_weapon = 0
    self.can_swim = 0
    for item in items_worn:
      self.stealth = self.stealth+item.info().stealth_boost
      if item.info().stealth_boost>0:
        self.stealth_texts.append(item.info().name)
      self.speed = self.speed+item.info().speed_boost
      if item.info().speed_boost>0:
        self.speed_texts.append(item.info().name)
      self.armor = self.armor+item.info().armor_boost
      if item.info().armor_boost>0:
        self.armor_texts.append(item.info().name)
      self.magic_resistance = self.magic_resistance+item.info().magic_resistance_boost
      if item.info().magic_resistance_boost>0:
        self.magic_resistance_texts.append(item.info().name)
      self.mp_regeneration_boost = self.mp_regeneration_boost+item.info().mp_regeneration_boost
      self.xp_boost = self.xp_boost + item.info().xp_boost
      self.melee_damage_boost = self.melee_damage_boost + item.info().melee_damage_boost
      if item.info().fire_resistance:
        fireres = fireres + 1
      if item.info().protected_from_missiles:
        self.protected_from_missiles = 1
      if item.info().poisoned_melee_weapon:
        self.poisoned_melee_weapon = 1
      if item.info().water_walking:
        self.can_swim = 1
    if fireres==0:
      self.fire_resistance = 0
      self.fire_immunity = 0
    elif fireres==1:
      self.fire_resistance = 1
      self.fire_immunity = 0
    else:
      self.fire_resistance = 0
      self.fire_immunity = 1
    self.avoids_fire = 0 # not while out of control, anyway
    self.avoids_pits = 0 # ditto
    self.avg_melee_damage = self.avg_melee_damage + self.melee_damage_boost
    if not quiet:
      for power in self.new_powers:
        if not power in self.powers and power in power_names.keys():
          display.show_power("New Power: ",power,power_names[power],power_descriptions[power])
    self.powers = self.new_powers
    self.powers.sort() # first power is tested first
    if not self.first_call:
      self.entity.update_pane(d)
    else:
      self.first_call = 0

  def add_powers(self,item):
    # called by the above
    if item is not None:
      if item.info().powers_granted is not None:
        for power in item.info().powers_granted:
          if not (power in self.new_powers):
            self.new_powers.append(power)
          else:
            upgrade = upgraded_version(power)
            if upgrade is not None and not (upgrade in self.new_powers):
              self.new_powers.append(upgrade)    

xp_requirements = [20,40,60,100,150,200,250,300,400,500,600,700,800,1000,1250,1500,1750,2000,1e6]
              
class Character(Entity):
  def __init__(self,d,pos):
    # Any new fields here - consider adding to new_dungeon() in gamestate.py
    self.name = "your hero"
    self.stats = Stats(d,self,self.name)
    self.score = 0
    self.level = 0
    Entity.__init__(self,d,G_PARTY,0,pos,M_CHARACTER,A_FRIENDLY,[])
    self.is_character = 1
    self.learned_powers = [] # fill in with P_ codes later - For list of actual powers available, see self.stats
    self.forgotten_powers = []
    self.powers_used = {} # key is a P_ code, entry is the turn last used (or no entry)
    self.recalc_hp_mp()
    self.original_zap = 10.0
    self.zap = self.original_zap  
    self.brain_when_out_of_control = Character_brain(d,self)
    self.melee_weapon = None # fill this in later
    self.missile_weapon = None
    self.garb = None # fill this in later
    self.items = [None,None,None,None,None,None] # fill these in later
    self.keys = [None,None,None,None,None] # fill these in later
    ### testing
    ### for power in range(1,27):
    ###  self.learned_powers.append(power)
    ###
    self.stats.recalc(d,quiet=1)

  def info(self):
    # overloaded for Characters - doesn't use the monster library
    return self.stats

  def get_displaynum(self):
    return self.id.member + 1
  
  def popup_image(self):
    if self.no_longer_here():
      return greyout(display.character_tile)
    else:
      return display.character_tile

  def update_pane(self,d):
    display.refill_character_pane(d)   

  def stealth_sound_multiplier(self):
    return pow(5.0 / max(0,self.info().stealth - 5),2)

  def pickup(self,d,pos,object):
    slot = object.info().slot
    ground = d.l.tile_at(pos).object
    if (slot==SL_GARB and self.garb is not None and self.garb.type==object.type) or (slot==SL_MELEE and self.melee_weapon is not None and self.melee_weapon.type==object.type) or (slot==SL_MISSILE and self.missile_weapon is not None and self.missile_weapon.type==object.type):
      return
    if slot<>SL_MONEY and slot<>SL_FOOD and object.info().is_chest_of_size==0:
      spare_pack_space = 0
      for item in self.items:
        if item is None:
          spare_pack_space = 1
      spare_key_space = 0
      for key in self.keys:
	if key is None:
	  spare_key_space = 1
      if not (slot==SL_GARB and self.garb is None) and not (slot==SL_MELEE and self.melee_weapon is None) and not (slot==SL_MISSILE and self.missile_weapon is None) and not (slot==SL_KEY and spare_key_space) and not (not (slot in [SL_GARB,SL_MELEE,SL_MISSILE,SL_KEY]) and spare_pack_space):
        pickup = display.info_popup(object=object,pickup_query=1)
        if not pickup:
          return
    is_chest = 0
    if object.info().is_chest_of_size>0:
      is_chest = 1
      n_items = random.randint(0,2*object.info().is_chest_of_size)
      if n_items==0:
        dud_chest = 1
      else:
        dud_chest = 1
        for i in range(n_items):
          good = random.random()<0.3
          difficulty = object.info().is_chest_of_quality
          random_object = None
          possible_objects,possible_object_probs,possible_good_objects,possible_good_object_probs = get_object_tables(difficulty,None,None)
          if good:
            for i in range(3):
              j = random.randint(0,len(possible_good_objects)-1)
              chosen_object = possible_good_objects[j]
              if random.random()<possible_good_object_probs[j] and chosen_object not in [O_CHEST_SMALL]: # TO DO: expand this list of chest exclusions            
                random_object = chosen_object
                break
          else:
            for i in range(3):
              j = random.randint(0,len(possible_objects)-1)
              chosen_object = possible_objects[j]
              if random.random()<possible_object_probs[j] and chosen_object not in [O_CHEST_SMALL]: # TO DO: expand this list of chest exclusions       
                random_object = chosen_object
                break            
          if random_object is not None:
            found_space = None
            for j in range(3):
              random_pos = pos.copy()
              random_pos.down = random_pos.down + random.randint(-1,1)
              random_pos.right = random_pos.right + random.randint(-1,1)
              if not (random_pos.down==pos.down and random_pos.right==pos.right):
                if d.l.tile_at(random_pos).object is None and d.l.tile_at(random_pos).info().can_contain_objects:
                  found_space = random_pos
                  dud_chest = 0
                  break
            if found_space is not None:
              d.l.tile_at(found_space).object = Object(random_object)
              occupant = d.get_occupant(found_space)
              display.redraw_tile(d,found_space,occupant)
    n = Notification(self.pos)      
    if slot==SL_MONEY:
      n.set_sound('coins')
    elif object.info().is_chest_of_size>0 and not dud_chest:
      n.set_sound('coins')
      n.set_message(self.name+" opens: "+object.info().name+".",0)
    elif object.info().is_chest_of_size>0 and dud_chest:
      n.set_sound('cancel1')
      n.set_message("The "+object.info().name+" is empty!",0)
    elif slot==SL_FOOD:
      if object.type<>O_SMALL_FOOD:
        n.set_sound("eatnoise-1")
      else:
        n.set_sound("munch2")
    else:
      n.set_sound("sword1")
    n.play(d)
    if slot<>SL_MONEY and slot<>SL_FOOD and slot<>SL_KEY and object.info().is_chest_of_size==0:
      spare_pack_space = 0
      for item in self.items:
        if item is None:
          spare_pack_space = 1
      if not (slot==SL_GARB and self.garb is None) and not (slot==SL_MELEE and self.melee_weapon is None) and not (slot==SL_MISSILE and self.missile_weapon is None) and not (not (slot in [SL_GARB,SL_MELEE,SL_MISSILE]) and spare_pack_space):
        pass
      else:
	display.info_popup(object=object)  
    if slot==SL_GARB:
      if self.garb is not None:
        d.l.tile_at(pos).object = self.garb
      else:
        d.l.tile_at(pos).object = None
      self.garb = object
    elif slot==SL_MELEE:
      if self.melee_weapon is not None:
        d.l.tile_at(pos).object = self.melee_weapon
      else:
        d.l.tile_at(pos).object = None
      self.melee_weapon = object    
    elif slot==SL_MISSILE:
      if self.missile_weapon is not None:
        d.l.tile_at(pos).object = self.missile_weapon
      else:
        d.l.tile_at(pos).object = None
      self.missile_weapon = object   
    elif slot==SL_MONEY:    
      self.score = self.score + int(object.info().value * (1+self.info().xp_boost))
      d.l.tile_at(pos).object = None
    elif object.info().is_chest_of_size>0:
      d.l.tile_at(pos).object = None
    elif slot==SL_FOOD:    
      self.hp = min(self.original_hp,self.hp + object.info().hp)
      self.heal_max_hp = min(self.original_hp,self.heal_max_hp + object.info().hp)
      d.l.tile_at(pos).object = None
    elif slot==SL_KEY:
      if self.keys[4] is not None and self.keys[3] is not None and self.keys[2] is not None and self.keys[1] is not None and self.keys[0] is not None:
        key_to_drop = 4
      else:
        key_to_drop = None
      if key_to_drop is not None:
        d.l.tile_at(pos).object = self.keys[key_to_drop]
      else:
        d.l.tile_at(pos).object = None
      if key_to_drop==0:
        self.keys = [object,self.keys[1],self.keys[2],self.keys[3],self.keys[4]]
      elif key_to_drop==1:
        self.keys = [self.keys[0],object,self.keys[2],self.keys[3],self.keys[4]]
      elif key_to_drop==2:
        self.keys = [self.keys[0],self.keys[1],object,self.keys[3],self.keys[4]]
      elif key_to_drop==3:
        self.keys = [self.keys[0],self.keys[1],self.keys[2],object,self.keys[4]]
      elif key_to_drop==4:
        self.keys = [self.keys[0],self.keys[1],self.keys[2],self.keys[3],object]
      elif self.keys[0] is None:
        self.keys = [object,self.keys[1],self.keys[2],self.keys[3],self.keys[4]]
      elif self.keys[1] is None:
        self.keys = [self.keys[0],object,self.keys[2],self.keys[3],self.keys[4]]
      elif self.keys[2] is None:
        self.keys = [self.keys[0],self.keys[1],object,self.keys[3],self.keys[4]]
      elif self.keys[3] is None:
        self.keys = [self.keys[0],self.keys[1],self.keys[2],object,self.keys[4]]
      elif self.keys[4] is None:
        self.keys = [self.keys[0],self.keys[1],self.keys[2],self.keys[3],object]      
    else:
      if self.items[5] is not None and self.items[4] is not None and self.items[3] is not None and self.items[2] is not None and self.items[1] is not None and self.items[0] is not None:
        item_to_drop = 5
      else:
        item_to_drop = None
      if slot<>SL_MISC_ITEM and slot<>SL_POTION:
        for i in range(6):
          if self.items[i] is not None:
            if self.items[i].info().slot==slot:
              item_to_drop = i
      if item_to_drop is not None:
        d.l.tile_at(pos).object = self.items[item_to_drop]
      else:
        d.l.tile_at(pos).object = None
      if item_to_drop==0:
        self.items = [object,self.items[1],self.items[2],self.items[3],self.items[4],self.items[5]]
      elif item_to_drop==1:
        self.items = [self.items[0],object,self.items[2],self.items[3],self.items[4],self.items[5]]
      elif item_to_drop==2:
        self.items = [self.items[0],self.items[1],object,self.items[3],self.items[4],self.items[5]]
      elif item_to_drop==3:
        self.items = [self.items[0],self.items[1],self.items[2],object,self.items[4],self.items[5]]
      elif item_to_drop==4:
        self.items = [self.items[0],self.items[1],self.items[2],self.items[3],object,self.items[5]]
      elif item_to_drop==5:
        self.items = [self.items[0],self.items[1],self.items[2],self.items[3],self.items[4],object]
      elif self.items[0] is None:
        self.items = [object,self.items[1],self.items[2],self.items[3],self.items[4],self.items[5]]
      elif self.items[1] is None:
        self.items = [self.items[0],object,self.items[2],self.items[3],self.items[4],self.items[5]]
      elif self.items[2] is None:
        self.items = [self.items[0],self.items[1],object,self.items[3],self.items[4],self.items[5]]
      elif self.items[3] is None:
        self.items = [self.items[0],self.items[1],self.items[2],object,self.items[4],self.items[5]]
      elif self.items[4] is None:
        self.items = [self.items[0],self.items[1],self.items[2],self.items[3],object,self.items[5]]
      elif self.items[5] is None:
        self.items = [self.items[0],self.items[1],self.items[2],self.items[3],self.items[4],object]
    if not is_chest:
      n = Notification(self.pos)    
      n.set_message(self.name+" picks up: "+object.info().name+".",0)
      n.play(d)
    self.info().recalc(d)
    display.redraw_tile(d,self.pos,self)
    display.show_glyphs(d)
    display.blit_map_to_screen()
    while self.score >= xp_requirements[self.level]:
      self.gain_level(d)
    if object.type==O_ORB_LIFE:
      files.win_game(d,display,"Congratulations - you have found the fabled Orb of Life!  Eternal fame and a thousand years of life lie before you...")
    
  def gain_level(self,d):
    self.level = self.level + 1
    new_power = self.next_power_to_gain()
    if new_power is None:
      display.simple_info_dialog("No more powers!","You have gained a level, but there is no new power you can learn. Try finding some different garb to wear!")
      return
    wait(500)
    display.send_message("You have gained a level!")
    sound.play('holy6')
    while len(self.learned_powers)>=12:
      power_to_forget = self.least_used_power()
      self.learned_powers.remove(power_to_forget)
      self.forgotten_powers.append(power_to_forget)
      display.show_power("You have forgotten an ability: ",power_to_forget,power_names[power_to_forget],power_descriptions[power_to_forget]) 
    self.learned_powers.append(new_power)
    self.powers_used[new_power] = d.turn
    self.recalc_hp_mp()
    self.info().recalc(d)

  def least_used_power(self):
    result = self.learned_powers[0]
    result_used_in_turn = self.powers_used.get(result,0)
    for power in self.learned_powers[1:]:
      this_power_used_in_turn = self.powers_used.get(power,0)
      if this_power_used_in_turn < result_used_in_turn:
        result = power
        result_used_in_turn = this_power_used_in_turn
    return result
    
  def next_power_to_gain(self):
    if self.garb is None:
      return None
    options = []
    for power in self.garb.info().possible_powers:
      if power in self.learned_powers:
        continue
      if power in self.forgotten_powers:
	continue
      requirements = power_requires.get(power,[])
      if len(requirements)>0:
        ok = 1
        for required in requirements:
          if not(required in self.learned_powers):
            ok = 0
        if not ok:
          continue
      not_in_first_n = power_not_in_first_n.get(power,0)
      if not_in_first_n>len(self.learned_powers):
        continue
      exclusions = power_requires_not.get(power,[])
      if len(exclusions)>0:
        ok = 1
        for excluded in exclusions:
          if excluded in self.learned_powers:
            ok = 0
        if not ok:
          continue      
      options.append(power)
    if len(options)==0:
      return None
    else:
      return options[random.randint(0,len(options)-1)]
    
  def recalc_hp_mp(self):
    if self.original_hp==0:
      hp_ratio = 1.0
      healable_hp_ratio = 1.0
      mp_ratio = 1.0
    else:
      hp_ratio = float(self.hp) / self.original_hp
      healable_hp_ratio = float(self.heal_max_hp) / self.heal_max_hp
      mp_ratio = float(self.mp) / self.original_mp
    self.original_hp = 40
    self.original_mp = 25
    for power in self.learned_powers:
      if powers_physical.get(power,1)==1:
        self.original_hp = self.original_hp + 10
      elif powers_physical.get(power,1)==0.5:
        self.original_hp = self.original_hp + 5
        self.original_mp = self.original_mp + 3.5
      else:
        self.original_mp = self.original_mp + 7
    self.hp = (self.original_hp * hp_ratio)
    self.heal_max_hp = (self.original_hp * healable_hp_ratio)
    self.mp = (self.original_mp * mp_ratio)
    # print self.original_hp, self.hp, self.heal_max_hp, self.original_mp, self.mp
  
  def use_zap(self,need,n):
    if self.zap > max(need,n):
      display.flash_zap_bar(self,yellow)
      self.zap = self.zap - n
      return 1
    else:
      return 0

  def make_run_away(self,d):
    wait(ms=400)
    n = Notification(self.pos)
    n.set_message("You are terrified!",0)
    n.play(d)
    self.add_condition(d,C_AFRAID,10,20)
    run_away = Priority_run_away(self.brain_when_out_of_control,urgency=PU_CRITICAL,while_condition_lasts=C_AFRAID,locked=1)
    self.brain_when_out_of_control.priorities.append(run_away)
    
def scatter_adjacent(d,entity,pos):
  # return a vacant spot next to pos and in the same room, or None if this is impossible
  room = d.l.room_at(pos)
  for dir in permute(DIRS_AROUND):
    new_pos = get_pos_in_dir(d,pos,dir)
    if new_pos is not None:
      tile = d.l.tile_at(new_pos)
      if tile.info().walkable(entity):
        occupant = d.get_occupant(new_pos)
        if occupant is None:
          return new_pos
  return None

# do some psyco bindings 
psyco.bind(Entity.__init__)
psyco.bind(Entity.after_action_update)
psyco.bind(Entity.begin_new_turn)
psyco.bind(Entity.check_conditions_expiry)
psyco.bind(Entity.has_condition)
psyco.bind(Entity.is_enemy)
psyco.bind(Entity.out_of_control)
psyco.bind(Entity.move)
psyco.bind(Group.get_next_actor)